/*
 * Bunisoft the Open Source Communications Company
 * Copyright 2006, Bunisoft Inc.,
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.teremail.panto;

import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;

/**
 * <p>An input stream that will read up to the end of a boundary.  It will 
 * return an EOF (-1) once that boundary has been reached.  It will only work
 * on top of a PushbackInputStream, as it will use a read ahead to efficiently
 * match against boundaries.  The PushbackInputStream will be positioned
 * just after the end of the boundary once the BoundaryInputStream has 
 * finished reading.  If the PushbackInputStream is read from before the 
 * BoundaryInputStream has returned EOF, the behaviour is undefined.</p>
 * 
 * <p>It uses the simple Boyer-Moore algorithm for determining when it has
 * reached a boundary.  The pattern is described as a byte array, so the user
 * must do any necessary charset encoding first</p>
 * 
 * @author Michael Barker
 * 
 */
public class BoundaryInputStream extends InputStream {

    private final PushbackInputStream in;
    private final SimpleBoyerMoore sbm;
    private final byte[] buffer;
    private int pos;
    private int limit;
    private int capacity;
    private boolean eof = false;
    private byte[] option;
    
    /**
     * Creates a boundary input stream on top an existing PushbackInputStream.
     * 
     * @param in The stream to read from.
     * @param pattern The byte pattern to look for.
     * @param size The size of buffer to use.
     */
    public BoundaryInputStream(PushbackInputStream in, byte[] pattern, int size) {
        this(in, pattern, new byte[0], size);
    }
    
    /**
     * Creates a boundary input stream on top an existing PushbackInputStream.
     * The option is any additional characters that the stream can skip over if
     * found directly after the boundary.
     * 
     * @param in The stream to read from.
     * @param pattern The byte pattern to look for.
     * @param option Some optional bytes to skip at the end of a boundary
     * @param size The size of buffer to use.
     */
    public BoundaryInputStream(PushbackInputStream in, byte[] pattern, byte[] option, int size) {
        this.in = in;
        buffer = new byte[size + pattern.length + option.length];
        pos = 0;
        limit = 0;
        sbm = new SimpleBoyerMoore(pattern);
        this.option = option;
    }
    
    /**
     * Read <code>len</code> bytes in the the byte array <code>buf</code> at the
     * offset <code>off</code>
     * 
     * @param buf The destination buffer to read into.
     * @param off The offset into the destination buffer.
     * @param len The number of bytes to read.
     * @return The number of bytes read or -1 if EOF.
     */
    public int read(byte[] buf, int off, int len) throws IOException {
        
        if (eof && limit <= pos) {
            return -1;
        }
        
        int numBytes = 0;
        
        do {
            if (limit <= pos) {
                eof = loadBuffer();
            }
            
            if (limit - pos > 0) {
                int toRead = Math.min(len - numBytes, limit - pos);
                System.arraycopy(buffer, pos, buf, numBytes + off, toRead);
                pos += toRead;
                numBytes += toRead;
            }
        } while (!eof && numBytes < len);
        
        return numBytes;
    }
    
    /**
     * Loads the internal buffer.  Will copy <code>tailLength</code> bytes
     * to the front of the buffer for the next read.
     * 
     * @param tailLength
     * @return true if eof reached.
     * @throws IOException 
     */
    private boolean loadBuffer() throws IOException {
        // Copy the remaining bytes to the front of the new buffer.
        int tailLength = capacity - pos;
        System.arraycopy(buffer, pos, buffer, 0, tailLength);
        capacity = tailLength;
        // Reset the position.
        pos = 0;
        // Read as much as we can into the new buffer.
        int numBytes = in.read(buffer, tailLength, buffer.length - tailLength);
        if (numBytes == -1) {
            // If there is nothing to read set the limit to the
            // remaining tail values.
            limit = tailLength;
            return true;
        } else {
            // Search the buffer for the pattern.
            capacity += numBytes;
            int idx = sbm.patternSearch(buffer, 0, capacity);
            if (idx == SimpleBoyerMoore.PATTERN_NOT_FOUND) {
                // Set the limit to the capacity, less the length of the
                // pattern.
                limit = capacity - (sbm.length() + option.length);
                return false;
            } else {
                // Set the limit to the index where the pattern is located.
                limit = idx;
                
                // Trim off any optional values that may occur before the
                // boundary.
                for (int i = option.length - 1; i >= 0 && limit > pos; --i) {
                    if (option[i] == buffer[limit - 1]) {
                        limit--;
                    } else {
                        break;
                    }
                }
                
                // Push back all of the values after the pattern.
                in.unread(buffer, idx + sbm.length(), capacity - (idx + sbm.length()));
                return true;
            }
        }
    }
    
    
    public int read() throws IOException {
        byte[] b = new byte[1];
        int num = read(b);
        if (num == -1 || num == 0) {
            return -1;
        }
        return (byte) b[0];
    }
    
    public int read(byte[] buf) throws IOException {
        return read(buf, 0, buf.length);
    }
}
